#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <fcntl.h>
#include <unistd.h>

#include <vorbis/vorbisfile.h>

#include "../au.h"
#include "../common.h"
#include "afile.h"

enum {
	A_NULL,
	A_SIZE,
	A_ENC,
	A_RATE,
	A_CHAN
};

struct afile {
	OggVorbis_File	 f;
	size_t		 o;
	int		 fd;
	int		 n;
};

static size_t	ovcb_read(void *, size_t, size_t, void *);
static int	ovcb_seek(void *, ogg_int64_t, int);
static int	ovcb_close(void *);
static long	ovcb_tell(void *);
static int	ov_errno(int);

static ov_callbacks ov_callbacks_fd = {
	&ovcb_read, &ovcb_seek, &ovcb_close, &ovcb_tell
};

const struct a_lookup stab[] = {
	{ "size", A_SIZE },
	{ "enc", A_ENC },
	{ "encoding", A_ENC },
	{ "rate", A_RATE },
	{ "chan", A_CHAN },
	{ "channel", A_CHAN },
	{ "channels", A_CHAN },
	{ NULL, A_NULL }
};

struct afile *
af_open(const char *file)
{
	int fd;

	if (file == NULL)
		ERR(EINVAL, NULL);
	if ((fd = open(file, O_RDONLY)) < 0)
		return (NULL);
	return (af_fdopen(fd));
}

struct afile *
af_fdopen(int fd)
{
	struct afile *a;

	if (fd < 0)
		ERR(EINVAL, NULL);
	if ((a = malloc(sizeof(struct afile))) == NULL)
		return (NULL);
	a->fd = fd;
	if (ov_open_callbacks(a, &a->f, NULL, 0, ov_callbacks_fd) !=
	    0) {
		free(a);
		return (NULL);
	}
	a->o = 2;
	a->n = 0;
	return (a);
}

void
af_close(struct afile *a)
{

	if (a == NULL)
		return;
	ov_clear(&a->f);
	free(a);
}

int
af_get(struct afile *a, const char *s, ...)
{
	vorbis_info *i;
	va_list ap;
	ssize_t r;
	int64_t i64;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	i = ov_info(&a->f, 0);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_SIZE:
			i64 = ov_pcm_total(&a->f, -1);
			if (a->o == 0 || i64 < 0)
				goto err;
			if (i64 > UINT32_MAX / a->o / i->channels)
				*va_arg(ap, uint32_t *) = UINT32_MAX;
			else
				*va_arg(ap, uint32_t *) = i64 * a->o *
				    i->channels;
			break;
		case A_ENC:
			switch (a->o) {
			case 1:
				*va_arg(ap, uint32_t *) = AU_PCM8;
				break;
			case 2:
				*va_arg(ap, uint32_t *) = AU_PCM16;
				break;
			default:
				goto err;
			}
			break;
		case A_RATE:
			*va_arg(ap, uint32_t *) = i->rate;
			break;
		case A_CHAN:
			*va_arg(ap, uint32_t *) = i->channels;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

int
af_set(struct afile *a, const char *s, ...)
{

	ERR(EINVAL, -1);
}

ssize_t
af_read(struct afile *a, void *b, size_t s)
{
	size_t r;

	if (a == NULL || b == NULL)
		ERR(EINVAL, -1);
	errno = 0;
	if ((r = ov_read(&a->f, b, s, 1, a->o, 1, &a->n)) == 0 &&
	    errno != 0)
		return (-1);
	return (r);
}

struct timespec
af_seek(struct afile *a, struct timespec ts, int whence)
{
	const struct timespec tserr = { -1, -1 };
	double c;
	double l;
	double t = (double)ts.tv_sec + (double)ts.tv_nsec /
	    (double)1000000000;
	int e;

	if (a == NULL)
		ERR(EINVAL, tserr);
	if (!ov_seekable(&a->f))
		ERR(EBADF, tserr);
	if ((c = ov_time_total(&a->f, -1)) < 0)
		ERR(ov_errno(c), tserr);
	switch (whence) {
	case SEEK_CUR:
		if ((l = ov_time_tell(&a->f)) < 0)
			ERR(ov_errno(l), tserr);
		if (t == 0)
			goto ret;
		break;
	case SEEK_END:
		l = c;
		t = -t;
		break;
	case SEEK_SET:
		l = 0;
		break;
	default:
		ERR(EINVAL, tserr);
	}
	if ((l += t) < 0)
		l = 0;
	if (l >= c)
		ERR(ERANGE, tserr);
	else {
		if ((e = ov_time_seek_lap(&a->f, l)) != 0)
			ERR(ov_errno(e), tserr);
		if ((l = ov_time_tell(&a->f)) < 0)
			ERR(ov_errno(l), tserr);
	}
ret:
	ts.tv_sec = (time_t)l;
	ts.tv_nsec = (long)(l * (double)1000000000);
	return (ts);
}

struct timespec
af_length(struct afile *a)
{
	const struct timespec tserr = { -1, -1 };
	struct timespec ts;
	double d;

	if ((d = ov_time_total(&a->f, -1)) < 0)
		ERR(ov_errno(d), tserr);
	ts.tv_sec = (time_t)d;
	ts.tv_nsec = (long)(d * (double)1000000000);
	return (ts);
}

size_t
ovcb_read(void *b, size_t s, size_t n, void *ctx)
{
	struct afile *a;
	ssize_t r;

	assert(ctx != NULL);
	a = ctx;
	if (SIZE_MAX / s < n)
		ERR(EOVERFLOW, 0);
	return ((r = read(a->fd, b, s * n)) < 0 ? 0 : r);
}

int
ovcb_seek(void *ctx, ogg_int64_t v, int whence)
{
	struct afile *a;

	assert(ctx != NULL);
	a = ctx;
	return (lseek(a->fd, v, whence) < 0 ? -1 : 0);
}

int
ovcb_close(void *ctx)
{
	struct afile *a;

	assert(ctx != NULL);
	a = ctx;
	if (ctx == NULL)
		ERR(EBADF, -1);
	return (close(a->fd));
}

long
ovcb_tell(void *ctx)
{
	struct afile *a;
	off_t r;

	assert(ctx != NULL);
	a = ctx;
	if ((r = lseek(a->fd, 0, SEEK_CUR)) > LONG_MAX)
		ERR(EOVERFLOW, -1);
	return (r);
}

int
ov_errno(int e)
{

	switch (e) {
	case OV_EREAD:
		return (EIO);
	case OV_EFAULT:
		return (EFAULT);
	case OV_EIMPL:
		return (EOPNOTSUPP);
	case OV_EINVAL:
		return (EINVAL);
	case OV_ENOTVORBIS:
		return (EFTYPE);
	case OV_EBADHEADER:
		return (EFTYPE);
	case OV_EVERSION:
		return (EOPNOTSUPP);
	case OV_EBADLINK:
		return (EILSEQ);
	case OV_ENOSEEK:
		return (EOPNOTSUPP);
	default:
		return (EINVAL);
	}
}
